package com.workingpub.commons.jdbc.mapping;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import com.workingpub.commons.domain.AssignedIdentifier;
import com.workingpub.commons.domain.Identifiable;
import com.workingpub.commons.domain.SequenceIdentifier;
import com.workingpub.commons.proxy.DomainEntityProxy;

public abstract class AbstractRowMapping<T> implements RowMapping<T> {
	protected final Logger logger = LoggerFactory.getLogger(getClass());
	private Map<String, Integer> columnInfos;

	@Override
	public T mapRow(Class<T> resultType, ResultSet rs, int rowNum) {
		try {
			fillColumnNames(rs);
			return mapping(resultType, rs, rowNum);
		} catch (Exception ex) {
			logger.error("Sql row mapping error : " + ex.getMessage(), ex);
			Assert.isNull(ex, ex.getMessage());
		}
		return null;
	}

	protected Map<String, Integer> getColumnInfos() {
		return columnInfos;
	}

	protected abstract T mapping(Class<T> resultType, ResultSet rs, int rowNum) throws Exception;

	protected Object convert(Class<?> type, ResultSet rs, String column) throws Exception {
		Object result = null;
		if (StringUtils.hasText(column)) {
			String colName = column.toUpperCase();
			if (!containColumn(colName)) {
				return null;
			}
			result = simpleConvert(type, rs, colName);
			if (null == result) {
				result = complexConvert(type, rs, colName);
			}
			if (null == result) {
				result = rs.getObject(colName);
			}
		}
		return result;
	}

	/**
	 * Key string are upper case
	 * 
	 * @param columnName
	 * @return
	 */
	protected boolean containColumn(String columnName) {
		if (null == columnInfos) {
			return false;
		}
		return columnInfos.containsKey(columnName.toUpperCase());
	}

	private void fillColumnNames(ResultSet rs) throws SQLException {
		if (null == columnInfos) {
			columnInfos = new HashMap<String, Integer>();
			ResultSetMetaData metaData = rs.getMetaData();
			for (int i = 1; i <= metaData.getColumnCount(); i++) {
				columnInfos.put(metaData.getColumnLabel(i).toUpperCase(), metaData.getColumnType(i));
			}
		}
	}

	private Object simpleConvert(Class<?> type, ResultSet rs, String colName) throws Exception {
		if (null == type) {
			return null;
		}
		if (String.class.isAssignableFrom(type)) {
			return rs.getString(colName);
		}
		if (Integer.class.isAssignableFrom(type)) {
			return rs.getInt(colName);
		}
		if (Long.class.isAssignableFrom(type)) {
			return rs.getLong(colName);
		}
		if (Boolean.class.isAssignableFrom(type)) {
			return rs.getBoolean(colName);
		}
		if (BigDecimal.class.isAssignableFrom(type)) {
			return rs.getBigDecimal(colName);
		}
		if (Float.class.isAssignableFrom(type)) {
			return rs.getFloat(colName);
		}
		if (Byte.class.isAssignableFrom(type)) {
			return rs.getByte(colName);
		}
		if (Double.class.isAssignableFrom(type)) {
			return rs.getDouble(colName);
		}
		if (Short.class.isAssignableFrom(type)) {
			return rs.getShort(colName);
		}
		if (Date.class.isAssignableFrom(type)) {
			return rs.getDate(colName);
		}

		if (Calendar.class.isAssignableFrom(type)) {
			Date time = rs.getTimestamp(colName);
			if (null != time) {
				Calendar calendar = Calendar.getInstance();
				calendar.setTime(time);
				return calendar;
			}
			return null;
		}
		if (type.isEnum()) {
			if (Types.VARCHAR == columnInfos.get(colName)) {
				Method method = type.getDeclaredMethod("valueOf", String.class);
				String value = rs.getString(colName);
				return StringUtils.hasText(value) ? method.invoke(null, value) : null;
			}
			if (Types.INTEGER == columnInfos.get(colName) || Types.BIGINT == columnInfos.get(colName)
					|| Types.NUMERIC == columnInfos.get(colName)) {
				Method method = type.getDeclaredMethod("values");
				Object[] enumValues = (Object[]) method.invoke(null);
				Integer index = Integer.valueOf(rs.getString(colName));
				return null == index ? null : index >= enumValues.length ? null : enumValues[index];
			}
		}
		return null;
	}

	private Object complexConvert(Class<?> type, ResultSet rs, String colName) throws Exception {
		if (null == type) {
			return null;
		}
		Object result = null;
		if (Identifiable.class.isAssignableFrom(type)) {
			if (SequenceIdentifier.class.isAssignableFrom(type)) {
				Method setMethod = type.getMethod("setId", Long.class);
				Long value = rs.getLong(colName);
				if (null != value) {
					if (Modifier.isAbstract(type.getModifiers())) {
						@SuppressWarnings("unchecked")
						DomainEntityProxy entityProxy = new DomainEntityProxy((Class<SequenceIdentifier>) type);
						result = entityProxy.create(value);
					} else {
						result = type.newInstance();
						setMethod.invoke(result, value);
					}
				}
			} else if (AssignedIdentifier.class.isAssignableFrom(type)) {
				Method setMethod = type.getMethod("setId", String.class);
				String value = rs.getString(colName);
				if (StringUtils.hasText(value)) {
					if (Modifier.isAbstract(type.getModifiers())) {
						@SuppressWarnings("unchecked")
						DomainEntityProxy entityProxy = new DomainEntityProxy((Class<SequenceIdentifier>) type);
						result = entityProxy.create(value);
					} else {
						result = type.newInstance();
						setMethod.invoke(result, value);
					}
				}
			} else {
				throw new IllegalArgumentException("Don't support resolve domain id type : " + type);
			}
		}
		return result;
	}
}
